/*
 *  Copyright 2008 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.taglib.core.syndication;

import com.blandware.atleap.webapp.util.core.ApplicationResources;
import com.blandware.atleap.webapp.util.core.ExternalResourceCache;
import com.blandware.atleap.webapp.util.core.GlobalProperties;
import com.blandware.atleap.webapp.util.core.WebappConstants;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.io.FeedException;
import com.sun.syndication.io.SyndFeedInput;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.taglib.TagUtils;
import org.xml.sax.InputSource;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;

/**
 * <p>This tag is used to read syndication feed and store it into context
 * variable. Feed stored is instance of class implementing {@link SyndFeed}.</p>
 * <p>Feed is read by URL, following formats are supported:
 * <ul>
 * <li>RSS 0.90</li>
 * <li>RSS 0.91 Netscape</li>
 * <li>RSS 0.91 Userland</li>
 * <li>RSS 0.92</li>
 * <li>RSS 0.93</li>
 * <li>RSS 0.94</li>
 * <li>RSS 1.0</li>
 * <li>RSS 2.0</li>
 * <li>Atom 0.3</li>
 * <li>Atom 1.0</li>
 * </ul></p>
 * <p>Feed is cached. When feed is already cached, cached version is put to
 * context, and after this feed cache is refreshed if its age is expired.
 * Refresh is made asynchronously. Maximum age of cached feed is defined by
 * build property named <code>externalResourceCache.cacheTime</code>.
 * </p>
 * <p>Following parameters are supported:
 * <ul>
 * <li><b>url</b> (required) - URL of feed to fetch</li>
 * <li><b>var</b> (required) - name of variable to which to put feed</li>
 * <li><b>scope</b> - scope of variable to which to put feed. Default is "page"</li>
 * <li><b>orderByDate</b> - whether feed entries need to be reordered by
 * publication date. Default is "false" (no reordering). This is not needed
 * usually because most feeds are already ordered by publication date.</li>
 * <li><b>orderByDateAsc</b> - whether entries need to be ordered by publication
 * date ascendingly if such ordering is made. Default is "false".</li>
 * <li><b>limit</b> - maximum count of entries to retain in feed. If omitted,
 * full list is retained.</li>
 * <li><b>ignore</b> - whether errors should be ignored. If this is true, no
 * error message will be shown on error, otherwise error message will be
 * rendered. Default is "false".</li>
 * </ul></p>
 * <p><a href="ReadFeedTag.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.2 $ $Date: 2008/01/20 12:22:58 $
 * @see com.sun.syndication.feed.synd.SyndFeed
 * @see com.sun.syndication.feed.synd.SyndEntry
 * @jsp.tag name="readFeed"
 * body-content="empty"
 */
public class ReadFeedTag extends SimpleTagSupport {

    /**
     * Logging instance
     */
    protected transient final Log log = LogFactory.getLog(ReadFeedTag.class);

    /**
     * Feed URL
     */
    protected String url;
    /**
     * Name of variable to which to export feed
     */
    protected String var;
    /**
     * Scope of variable to which to export feed
     */
    protected String scope;
    /**
     * Whether to order feed entries by publication date
     */
    protected Boolean orderByDate = Boolean.FALSE;
    /**
     * Whether ordering must be done ascendingly
     */
    protected Boolean orderByDateAsc = Boolean.FALSE;
    /**
     * Maximum number of entries to output
     */
    protected Integer limit = null;
    /**
     * Whether error must be silently ignored
     */
    protected Boolean ignore = Boolean.FALSE;

    /**
     * Returns feed URL.
     *
     * @return URL
     * @jsp.attribute required="true"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="URL of feed to fetch"
     */
    public String getUrl() {
        return url;
    }

    /**
     * Sets feed URL.
     *
     * @param url   URL to set
     */
    public void setUrl(String url) {
        this.url = url;
    }

    /**
     * Returns name of context variable to which to put feed.
     *
     * @return variable name
     * @jsp.attribute required="true"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Name of variable to export feed"
     */
    public String getVar() {
        return var;
    }

    /**
     * Sets name of context variable to which to put feed.
     *
     * @param var   name to set
     */
    public void setVar(String var) {
        this.var = var;
    }

    /**
     * Returns scope of context variable to which to put feed.
     *
     * @return scope
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.String"
     * description="Scope of variable to export feed"
     */
    public String getScope() {
        return scope;
    }

    /**
     * Sets scope of context variable to which to put feed.
     *
     * @param scope     scope to set
     */
    public void setScope(String scope) {
        this.scope = scope;
    }

    /**
     * Returns whether to order feed entries by publication date.
     *
     * @return true if need to order
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.Boolean"
     * description="Whether to order feed entries by publication date"
     */
    public Boolean getOrderByDate() {
        return orderByDate;
    }

    /**
     * Sets whether to order feed entries by publication date.
     *
     * @param orderByDate   true if need to order
     */
    public void setOrderByDate(Boolean orderByDate) {
        this.orderByDate = orderByDate;
    }

    /**
     * Returns whether ordering is ascending.
     *
     * @return true if ascending
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.Boolean"
     * description="Whether ordering is ascending"
     */
    public Boolean getOrderByDateAsc() {
        return orderByDateAsc;
    }

    /**
     * Sets whether ordering is ascending.
     *
     * @param orderByDateAsc    true if ascending
     */
    public void setOrderByDateAsc(Boolean orderByDateAsc) {
        this.orderByDateAsc = orderByDateAsc;
    }

    /**
     * Returns maximum number of entries to retain in feed.
     *
     * @return items count limit
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.Integer"
     * description="Maximum number of entries to retain in feed"
     */
    public Integer getLimit() {
        return limit;
    }

    /**
     * Sets maximum number of entries to retain in feed.
     *
     * @param limit     limit to set
     */
    public void setLimit(Integer limit) {
        this.limit = limit;
    }

    /**
     * Returns whether error must be silently ignored.
     *
     * @return true if error should be ignored
     */
    public Boolean getIgnore() {
        return ignore;
    }

    /**
     * Sets whether error must be silently ignored.
     *
     * @param ignore    value to set
     * @jsp.attribute required="false"
     * rtexprvalue="true"
     * type="java.lang.Boolean"
     * description="Whether error must be silently ignored"
     */
    public void setIgnore(Boolean ignore) {
        this.ignore = ignore;
    }

    /**
     * Does main tag job: reads syndication feed and stores it in context.
     *
     * @throws JspException
     */
    public void doTag() throws JspException {

        if (orderByDate == null) {
            orderByDate = Boolean.FALSE;
        }
        if (orderByDateAsc == null) {
            orderByDateAsc = Boolean.FALSE;
        }
        if (ignore == null) {
            ignore = Boolean.FALSE;
        }

        try {
            PageContext pageContext = (PageContext) getJspContext();
            ServletContext servletContext = pageContext.getServletContext();
            GlobalProperties globalProperties = GlobalProperties.getInstance(servletContext);
            Integer cacheTime = globalProperties.getInteger(WebappConstants.GLOBAL_PROPERTY_EXTERNAL_RESOURCE_CACHE_TIME);
            ExternalResourceCache externalResourceCache = ExternalResourceCache.getInstance(servletContext);
            ExternalResourceCache.ResourceData resourceData = externalResourceCache.loadExternalResource(url, cacheTime.intValue(), true);

            SyndFeed syndFeed;
            byte[] data = resourceData.getContent();
            SyndFeedInput input = new SyndFeedInput();
            syndFeed = input.build(new InputSource(new ByteArrayInputStream(data)));

            // sorting by publication date if needed
            if (orderByDate.booleanValue()) {
                Collections.sort(syndFeed.getEntries(), new Comparator() {
                    public int compare(Object o1, Object o2) {
                        SyndEntry e1 = (SyndEntry) o1;
                        SyndEntry e2 = (SyndEntry) o2;
                        int result;
                        boolean date1IsNull = e1.getPublishedDate() == null;
                        boolean date2IsNull = e2.getPublishedDate() == null;
                        if (date1IsNull && date2IsNull) {
                            result = 0;
                        } else if (!date1IsNull && !date2IsNull) {
                            result = e1.getPublishedDate().compareTo(e2.getPublishedDate());
                        } else if (date1IsNull) {
                            result = 1;
                        } else {
                            result = -1;
                        }
                        if (!orderByDateAsc.booleanValue()) {
                            result = -result;
                        }
                        return result;
                    }
                });
            }

            // limiting entry count if needed
            if (limit != null) {
                syndFeed.setEntries(syndFeed.getEntries().subList(0, limit.intValue()));
            }

            TagUtils tagUtils = TagUtils.getInstance();
            int varScope = PageContext.PAGE_SCOPE;
            if ( scope != null ) {
                varScope = tagUtils.getScope(scope);
            }
            pageContext.setAttribute(var, syndFeed, varScope);
        } catch (IOException e) {
            processException(e);
        } catch (FeedException e) {
            processException(e);
        }
    }

    /**
     * Logs exception. Also, if <b>ignore</b> is false, shows an error message.
     *
     * @param e     exception we are processing
     * @throws javax.servlet.jsp.JspException if write to JSP failed
     * @see #ignore
     */
    protected void processException(Exception e) throws JspException {
        if (log.isWarnEnabled()) {
            log.warn("Cannot get feed: ", e);
        }
        if (!ignore.booleanValue()) {
            PageContext pageContext = (PageContext) getJspContext();
            HttpServletRequest request = (HttpServletRequest) pageContext.getRequest();
            ApplicationResources applicationResources = ApplicationResources.getInstance(pageContext.getServletContext());
            String message = applicationResources.getMessage(request, "core.commons.readFeedTag.cannotReadFeed");
            TagUtils tagUtils = TagUtils.getInstance();
            tagUtils.write(pageContext, message);
        }
    }
}
